<?php
/**
 * dbfront
 * (c) 2010-2013 Stephen Adkins <spadkins@gmail.com>
 * This software began development in 2010.
 * It is based on code dating from 1999.
 *
 * License: GPL 2.0
 * The contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL").
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 * */

namespace API;

class ObjectSet {
    /**
     * execute_object_db_table() - implements the query requirements of the ExtJS Grid component
     * The following are the parameters recognized.
     * @param string  database        - [IN] the database (a connection)      (required on all queries)
     * @param string  table           - [IN] the table to operate on          (required on all queries)
     * @param string  columns         - [IN] comma-separated list of columns  (required on all queries)
     * @param string  view            - [IN] (optional) the view specifier which is used to retrieve saved supplemental user interface information ({db}.{table} or {db}.{table}.{userview})
     * @param integer start           - [IN] the first row number of the query data to be returned to the client (starting at row 1) (skip over rows before this one and don't return them)
     *                                       (this is technically optional, but it is sensible to configure your Ext program to always send this value for result set paging)
     * @param integer limit           - [IN] the maximum number of rows of the query data to be returned to the client
     *                                       (this is technically optional, but it is sensible to configure your Ext program to always send this value for result set paging)
     * @param string  sort            - [IN] json-encoded array of sort columns (e.g. [{"property":"alt_city_nm","direction":"ASC"}])
     * @param string  rowtype         - [IN] "array" or "object" (default is "array") specifies the format of the returned data (either will work with Ext.Grid)
     * @param string  primary_key     - [IN] comma-separated list of columns that make up the primary key (for updating)
     * @param string  p-{column}      - [IN] filter (where clause) parameters (implied =/IN operation)
     * @param string  p-{column}-{op} - [IN] filter (where clause) parameters (explicit operation)
     * @param string  meta            - [IN] (boolean) in addition to returning the data in the {data} attribute, also return the table metadata in the {meta} attribute
     * @param string  neat            - [IN] used for debugging, this puts extra newlines in the returned JSON
     * @param string  nocount         - [IN] inhibit the extra query that produces the totalCount element used for Grid paging
     * @param boolean success         - [OUT] whether the query succeeded
     * @param string  message         - [OUT] error message (only if success is false)
     * @param array   data            - [OUT] the result rows that were requested
     * @param integer total           - [OUT] count of all of the rows that would have been returned if there were no filters (for result set paging)
     * @param string  sql             - [OUT] the SQL statement used to query the data
     * @param string  table           - [OUT] (advisory) the table name
     * @param string  params          - [OUT] (advisory) the params that were part of filters, etc.
     * */
    public function execute_object_db_table ($http_method, $request, &$response, $db, $table) {
        global $context, $trace;
        if ($trace) trace_entry();

        if (isset($request['op'])) $http_method = $this->get_effective_http_method($http_method, $request['op']);

        if ($http_method === 'get') {      # SQL SELECT

            $get_options = array();
            # [x]view, relationship, [x]aggregate, columns, group_columns, filter, sort, [x]limit)
            if (isset($request['view']))          { $get_options['view']          = $request['view'];          }
            if (isset($request['table_def']))     { $get_options['table_def']     = $request['table_def'];     }
            if (isset($request['relationships'])) { $get_options['relationships'] = $request['relationships']; }
            if (isset($request['aggregate']))     { $get_options['aggregate']     = ($request['aggregate'] && $request['aggregate'] !== 'false') ? 1 : 0; }
            if (isset($request['columns']))       { $get_options['columns']       = explode(',',$request['columns']); }
            if (isset($request['group_columns'])) { $get_options['group_columns'] = $request['group_columns']; }
            if (isset($request['data_columns']))  { $get_options['data_columns']  = explode(',',$request['data_columns']); }
            if (isset($request['keys_horiz']))    { $get_options['keys_horiz']    = $request['keys_horiz']; }

            $table_def   = &$db->get_table_def($table, null, $get_options);

            $request['table'] = $table;
            if (!isset($request['columns'])       && isset($get_options['columns']))       { $request['columns']       = $get_options['columns']; }
            if (!isset($request['data_columns'])  && isset($get_options['data_columns']))  { $request['data_columns']  = $get_options['data_columns']; }
            if (!isset($request['keys_horiz'])    && isset($get_options['keys_horiz']))    { $request['keys_horiz']    = $get_options['keys_horiz']; }
            if (!isset($request['group_columns']) && isset($get_options['group_columns'])) { $request['group_columns'] = implode(',',$get_options['group_columns']); }
            if (!isset($request['aggregate'])     && isset($get_options['aggregate']))     { $request['aggregate']     = $get_options['aggregate']; }
            if (!isset($request['filter'])        && isset($table_def['filter']))          { $request['filter']        = $table_def['filter']; }
            if (!isset($request['sort'])          && isset($table_def['sort']))            { $request['sort']          = $table_def['sort']; }
            if (!isset($request['limit'])         && isset($table_def['limit']))           { $request['limit']         = $table_def['limit']; }

            $this->query($request, $response, $db, $table, $table_def);

            $response['success'] = true;
            $response['table']   = $table;
            $response['params']  = $request;
            if (isset($request['meta']) && $request['meta']) {
                $response['meta'] = $table_def;
            }
        }
        elseif ($http_method === 'post') { # SQL INSERT
            if (!is_array($request)) throw new \Exception('Neither a row of data nor an array of rows was provided');
            if ($db->is_assoc($request)) {
                $response['nrows']   = $db->insert($table, $request, null, array('update'=>1,'last_insert_id'=>1,'init_not_null'=>1));
            }
            else {
                $response['nrows']   = $db->insert_rows($table, $request, null, array('update'=>1,'last_insert_id'=>1,'init_not_null'=>1));
            }
            $response['data']    = $request;
            $response['message'] = 'Created or updated row';
            $response['success'] = true;
        }
        elseif ($http_method === 'put') {  # SQL UPDATE
            if (count($request) > 0) {
                if ($db->is_assoc($request)) {
                    $nrows = $db->update($table, $request);
                }
                else {
                    $nrows = 0;
                    foreach ($request as $params) {
                        $nrows += $db->update($table, $params);
                    }
                }
                $response['nrows']  = $nrows;
                $response['table']  = $table;
                $response['params'] = $request;
                if ($nrows > 0) {
                    $response['message'] = ($nrows == 1) ? 'Updated 1 row' : "Updated $nrows rows";
                    $response['success'] = true;
                }
                else {
                    $response['message'] = 'Update failed. Row not found.';
                    $response['success'] = false;
                }
            }
            else {
                $response['message'] = 'Update failed. No parameters given to limit the update.';
                $response['success'] = false;
            }
        }
        elseif ($http_method === 'delete') {  # SQL DELETE
            if (count($request) > 0) {
                if ($db->is_assoc($request)) {
                    $nrows = $db->delete($table, $request);
                }
                else {
                    $nrows = 0;
                    foreach ($request as $params) {
                        $nrows += $db->delete($table, $params);
                    }
                }
                $response['nrows']  = $nrows;
                $response['table']  = $table;
                $response['params'] = $request;
                if ($nrows > 0) {
                    $response['message'] = ($nrows == 1) ? 'Deleted 1 row' : "Deleted $nrows rows";
                    $response['success'] = true;
                }
                else {
                    $response['message'] = 'Delete failed. Row not found.';
                    $response['success'] = false;
                }
            }
            else {
                $response['message'] = 'Delete failed. No parameters given to limit the delete.';
                $response['success'] = false;
            }
        }
        else {
            throw new \Exception('Method Not Allowed', 405);
        }
        if ($trace) trace_exit();
    }

    public function execute_object_db_table_key ($http_method, $request, &$response, $db, $table, $key) {
        global $context, $trace;
        if ($trace) trace_entry();

        if (isset($request['op'])) $http_method = $this->get_effective_http_method($http_method, $request['op']);

        if ($http_method === 'get') {        # SQL SELECT
            $columns            = isset($request['columns']) ? explode(',',$request['columns']) : null;
            $response['data']   = $db->get_hash($table, $key, $columns);
            $response['table']  = $table;
            $response['key']    = $key;
        }
        elseif ($http_method === 'put') {    # SQL UPDATE
            $nrows = $db->update($table, $key, $request);
            $response['nrows']  = $nrows;
            $response['table']  = $table;
            $response['key']    = $key;
            if ($nrows > 0) {
                $response['message'] = ($nrows == 1) ? 'Updated 1 row' : "Updated $nrows rows";
                $response['success'] = true;
            }
            else {
                $response['message'] = 'Update failed. Row not found.';
                $response['success'] = false;
            }
        }
        elseif ($http_method === 'delete') { # SQL DELETE
            $nrows = $db->delete($table, $key);
            $response['nrows']  = $nrows;
            $response['table']  = $table;
            $response['key']    = $key;
            if ($nrows > 0) {
                $response['message'] = ($nrows == 1) ? 'Deleted 1 row' : "Deleted $nrows rows";
                $response['success'] = true;
            }
            else {
                $response['message'] = 'Delete failed. Row not found.';
                $response['success'] = false;
            }
        }
        else {
            throw new \Exception('Method Not Allowed', 405);
        }
        $response['success'] = true;
        if ($trace) trace_exit();
    }

    /**
     * query($request, $response) - invoked by the URL "http://.../api/object/{database}/{table}" API (e.g. "http://.../api/object/log/request")
     * @param string  view            - [IN] a view specifier (eliminates the need for columns, aggregate, sort, filter) (of the form {database}.{table} or {database}.{table}.{viewlabelstr})
     * @param string  columns         - [IN] comma-separated list of column names that generally reflect columns that the user interface could display (but may be temporarily hidden)
     * @param integer nocount         - [IN] (pseudo-boolean) do not return the {total} number of rows in the response (to save the extra query)
     * @param integer start           - [IN] (default is 1)   starting row number (starting from 1) of the results set to be returned
     * @param integer limit           - [IN] (default is 100) number of total rows to return (if limit=0, all rows are returned)
     * @param integer aggregate       - [IN] (pseudo-boolean) perform an aggregate query. all key columns are group-by keys (string, integer/ID, date, datetime-to-hour). all non-key columns are aggregated with sum().
     * @param string  visible_columns - [IN] used when an aggregate query is done so that the non-visible columns can be removed from the query, or with keys_horiz to filter resulting columns to only the list that will be visible
     * @param string  group_columns   - [IN] used in aggregate mode. query only the display_columns which are either (a) aggregate keys and in the group_columns, or (b) not aggregate keys
     * @param string  keys_horiz      - [IN] names a column or list of columns whose values will be appended to every non-key column to create a cross-tabulated set of columns (used often for graphs)
     * @param string  sql             - [IN] specify the exact SQL statement to run instead of building it up from columns, filters, aggregate, sort, etc.
     * @param string  sort (form 1)   - [IN] the value for sort may be in either of two forms: (form 1) a JSON-encoded array of associative arrays which are called sorts.
     *          e.g. [{"property":"log_date","direction":"DESC"},{"property":"remote_addr","direction":"ASC"}]
     *        > property :   (required) the name of the sort. generally, this is the column name of the table being sorted.
     *        > direction :  (optional) the direction of the sort. This should be 'ASC' or 'DESC' to sort forward/ascending or backward/descending. (default is ASC)
     * @param string  sort (form 2)   - [IN] (form 2) a comma-separated list of columns to be sorted, optionally with direction specifiers appended to the column names.
     *          e.g. log_date.DESC,remote_addr.ASC
     * @param string  filter          - [IN] a JSON-encoded array of associative arrays which are each called filters. Each filter contains the following attributes.
     *          e.g. [{"property":"remote_addr","value":"184.47.47.50","type":"string","operator":"eq"}]
     *        > property :   (required) the name of the filter. generally, this is the column name of the table being filtered.
     *        > value :      (required) the value of the filter. generally, this is the column name of the table being filtered.
     *        > type :       (optional) the type of the field is inferred from the value. this can be made explicit if a type is supplied.
     *                       numeric    column type in the database is integer or float (understands numeric inequality)
     *                       string     column type in the database is char or varchar (understands string inequality. value requires quoting and escaping.)
     *        > operator :   (optional) the operator of the filter is inferred from the field and value (in a DWIM-way). this can be overridden if an operator is supplied.
     *                       > eq       equal to     (SQL: where r.remote_addr = '184.47.47.50')
     *                       > ne       not equal to (SQL: where r.remote_addr != '184.47.47.50')
     *                       > gt       greater than (SQL: where r.remote_addr > '184.47.47.50')
     *                       > lt       less than    (SQL: where r.remote_addr < '184.47.47.50')
     *                       > ge       greater than (SQL: where r.remote_addr >= '184.47.47.50')
     *                       > le       less than    (SQL: where r.remote_addr <= '184.47.47.50')
     *                       > in       in set       (SQL: where r.remote_addr in ('184.47.47.50','184.47.47.59'))
     *                       > contains the value string is a substring of the column value                                  (SQL: where r.remote_addr like '%184.47.47.50%')
     *                       > matches  the value string is a DOS-like (glob) pattern match for the column value (e.g. ?TL*) (SQL: where r.remote_addr like '_TL%')
     *        > field :      (a synonym for 'property'. either name can be used.)
     *        > comparison : (a synonym for 'operator'. either name can be used.)
     *        > this is how inferred operators work (with examples) ...
     *          * Numeric Fields (type='numeric') (the following are the inferences made from the given values)
     *            5       - a single number will match that number only
     *            1-5     - a numeric range will match any number in the range
     *            1,3,5   - a numeric list that will match any number in the list
     *            >5 OR <5 OR >=5 OR <=5 OR !5 OR !=5 OR <>5 OR =5 - a single number with a logical comparison test will do the expected thing
     *          * Character/String Fields (type='string') (the following are the inferences made from the given values)
     *            =ATL                       - matches that exact string of text
     *            ATL* OR ATL???A OR *TA     - the use of * and ? characters will create a DOS-like pattern match for all strings that apply (*=any number of characters, ?=any single character) (all character searches are case-insensitive)
     *            ATL                        - any text fragment made up of only alphabetic characters, spaces, and periods ([A-Za-z \.]) will match if the text exists anywhere in the column (same as *ATL*, but it is convenient because it gets progressively more specific as you type) (a string made up of anything besides letters, spaces, and periods should be prefixed with "~" to get the same effect)
     *            >ATL OR <ATL OR >=ATL OR !ATL OR !=ATL OR <>ATL - a single string with a logical comparison test will do the expected thing
     *            ATL,LGA,DCA                - a list of strings (indicated by the existence of commas) will match exactly any of the strings in the list
     *            =ATL,LGA,DCA               - matches the exact string (including commas or whatever)
     *            =I said "Oh, #@$%^&*"!?!   - match exactly an arbitrary piece of text
     *            ~VH1                       - same as *VH1* the string is contained somewhere in the target column
     *            =~GDS_*                    - same as GDS_*, the string must match exactly but it may include DOS-like pattern matching (wildcard/glob) characters (* and ?)
     *            A-J                        - a string range will match any strings that start with letters in the range
     *          * Date/Datetime Fields (type='date' or 'datetime') (the following are the inferences made from the given values)
     *            -1                         - a numeric constant on a date or datetime column will match all dates/datetimes from the current time to the current time plus the specified number of days
     *            -7--3 OR 1-14              - a numeric range on a date or datetime column will match all dates/datetimes in the range of days relative to the current time
     *            20121225 OR 2012-12-05                      - will match a single date
     *            20121225120000 OR 2012-12-05 12:00:00       - will match a single datetime
     *            20121225-20121226                           - will match a range of dates
     *          * All Fields (the following are the inferences made from the given values)
     *            NULL                       - the word NULL will match if the column is NULL
     *            !NULL                      - the word !NULL will match if the column is NOT NULL
     *            *                          - this acts as though the filter does not exist, but it also excludes the column from being a key in aggregate queries
     * @param boolean success         - [OUT] tells whether the query executed correctly
     * @param string  message         - [OUT] if unsuccessful, this holds the error message
     * @param object  data            - [OUT] the data rows returned from the query
     * @param integer total           - [OUT] the total number of rows (estimated) that would be returned if start=1 and limit=0
     * @param string  sql             - [OUT] the actual SQL statement that was run
     * NOTE: the following parameters exist on the web service that calls the query() method, but do not exist on the query() method itself
     * @param integer meta            - [IN]  (pseudo-boolean) indicates that the metadata for the table/query should be returned along with the data
     * @param string  table           - [OUT] (advisory) the table that was requested
     * @param object  params          - [OUT] (advisory) various parameters that were used in the request
     * @param object  meta            - [OUT] various parameters that were used in the request
     */
    public function query ($request, &$response, $db, $table, &$table_def = null) {
        global $context, $options, $trace;
        if ($trace) trace_entry();

        $sql = null;

        if (isset($request['schema'])) {
            $schema = $request['schema'];
            if (! preg_match('/^[a-zA-Z0-9_]+$/', $schema)) throw new \Exception('ERROR: the "schema" parameter contains illegal characters (columns must be letters, digits, and underscores)');
            $fulltable = "$schema.$table";
        }
        else $fulltable = $table;

        $get_options = array();
        if (isset($request['view']))      $get_options['view']      = $request['view'];
        if (isset($request['table_def'])) $get_options['table_def'] = $request['table_def'];

        if (isset($request['rowtype'])) {
            $rowtype = $request['rowtype'];
            if ($rowtype != 'array' && $rowtype != 'object') throw new \Exception('ERROR: the "rowtype" parameter is an illegal value (must be "array" or "object")');
        }
        else {
            $rowtype = 'object';
        }

        $matches = array();
        $response['success'] = true;   # assume we are going to succeed, whatever it is we are doing

        if (isset($request['sql']) && $request['sql']) {
            $sql = $request['sql'];

            ###################################################################################################
            # Data Query
            ###################################################################################################

            $nocount   = (isset($request['nocount'])   && $request['nocount'])   ? 1 : 0;
            $neat      = (isset($request['neat'])      && $request['neat'])      ? 1 : 0;

            ###################################################################################################
            # Query Options
            ###################################################################################################

            if (isset($request['start']) && $request['start'] > 0) {
                $start = $request['start'];
                if (! preg_match('/^[0-9]+$/', $start)) throw new \Exception('ERROR: the "start" parameter contains illegal characters (must be a non-negative integer)');
                $get_options['offset'] = $start;
            }
            if (isset($request['limit'])) {
                $limit = $request['limit'];
                if (! preg_match('/^[0-9]+$/', $limit)) throw new \Exception('ERROR: the "limit" parameter contains illegal characters (must be a non-negative integer)');
                if ($limit) $get_options['limit'] = $limit;
            }

            if (isset($options['test_mode']) && $options['test_mode']) {
                if (isset($request['debug_sql'])) {
                    if (! preg_match('/^-?[0-9]+$/', $request['debug_sql'])) throw new \Exception('ERROR: the "debug_sql" parameter contains illegal characters (must be an integer)');
                    global $debug_sql;
                    $debug_sql = $request['debug_sql'];
                }
                if (isset($request['trace'])) {
                    if (! preg_match('/^-?[0-9]+$/', $request['trace'])) throw new \Exception('ERROR: the "trace" parameter contains illegal characters (must be an integer)');
                    global $trace;
                    $trace = $request['trace'];
                }
            }

            if ($rowtype === 'array') {
                $data = $db->get_rows($sql, null, null, $get_options);
            }
            else {
                $data = $db->get_hashes($sql, null, null, $get_options);
            }
            $response['data'] = $data;
            $response['sql']  = $sql;

            if (!$nocount) {
                $count_options = $get_options;
                if (isset($count_options['offset'])) {
                    $count_options['limit'] = $count_options['offset'] + 1000;
                    unset($count_options['offset']);
                }
                else {
                    $count_options['limit'] = 1000;
                }
                $sql = preg_replace('/[ \\t\\n]+order +by.*/is', '', $sql);

                $db->add_limit_clause($sql, $count_options);
                $count_rows = $db->get_rows("select count(*) from ($sql) x");
                $count = $count_rows[0][0];

                $response['total'] = $count;
            }
        }
        else {
            if (! isset($table) || !$table) throw new \Exception('ERROR: the "table" parameter must be supplied');
            if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) throw new \Exception('ERROR: the "table" parameter contains illegal characters (must be letters, digits, and underscores)');

            if (isset($request['columns']) && $request['columns']) {
                $columns = $request['columns'];
                if (is_scalar($columns)) {
                    if (! preg_match('/^[a-zA-Z0-9_,]+$/', $columns)) throw new \Exception("ERROR: the 'columns' parameter contains illegal characters (columns must be letters, digits, and underscores, separated by commas) (perhaps with column-modifiers delimited by double-underscores) ($columns)");
                    $columns = explode(',', $columns);
                }
            }
            else {
                $columns = null;
            }

            if (isset($request['visible_columns']) && $request['visible_columns']) {
                $visible_columns = $request['visible_columns'];
                if (! preg_match('/^[a-zA-Z0-9_,]+$/', $visible_columns)) throw new \Exception('ERROR: the "visible_columns" parameter contains illegal characters (visible_columns must be letters, digits, and underscores, separated by commas)');
                $visible_columns = explode(',', $visible_columns);
            }
            else {
                $visible_columns = null;
            }

            if (isset($request['data_columns']) && $request['data_columns']) {
                $data_columns = $request['data_columns'];
                if (! preg_match('/^[a-zA-Z0-9_,:]+$/', $data_columns)) throw new \Exception('ERROR: the "data_columns" parameter contains illegal characters (data_columns must be letters, digits, and underscores, separated by commas) (perhaps with column-modifiers delimited by double-underscores)');
                $data_columns = explode(',', $data_columns);
            }
            else {
                $data_columns = null;
            }

            $is_aggregate_query = 0;
            $group_columns     = (isset($request['group_columns'])) ? $request['group_columns'] : null;
            if ((isset($request['aggregate']) && $request['aggregate'] && $request['aggregate'] !== 'false') || isset($group_columns)) {
                $is_aggregate_query = 1;
                $get_options['aggregate'] = 1;
            }

            #######################################################################################
            # give $columns an appropriate default value (unless the query itself can generate the columns)
            #######################################################################################
            $can_generate_columns = (isset($request['keys_horiz'])) ? true : false;
#echo "columns=[$columns]\n";
#echo "can_generate_columns=[$can_generate_columns]\n";
            if (isset($columns)) {
                if (!isset($data_columns)) {
                    $data_columns = $columns;      # the column list is really the set of data columns we want to query
                    if ($can_generate_columns) {
                        $columns = null;           # the real column list will be generated
                    }
                }
            }
            else {
                if ($can_generate_columns && isset($data_columns)) {
                    # do nothing. the column list will be generated
                }
                elseif (isset($visible_columns)) {
                    $columns = $visible_columns;
                }
                else {
                    if (!isset($table_def)) $table_def = &$db->get_table_def($table, null, $get_options);
                    if (isset($table_def['default_columns'])) {
                        $columns = $table_def['default_columns'];
                    }
                    elseif (isset($table_def['phys_columns'])) {
                        $columns = $table_def['phys_columns'];
                    }
                    elseif (isset($table_def['columns'])) {
                        $columns = $table_def['columns'];
                    }
                    else {
                        throw new \Exception('ERROR: could not determine the "columns" parameter');
                    }
                }
            }

            if (isset($columns) && !isset($data_columns)) {
                $data_columns = $columns;
            }

            ###################################################################################################
            # Data Query
            ###################################################################################################

            $nocount   = (isset($request['nocount'])   && $request['nocount'])   ? 1 : 0;
            $neat      = (isset($request['neat'])      && $request['neat'])      ? 1 : 0;

            ###################################################################################################
            # Start with the Base Params and Add the User-Selected Dynamic Filters to the Params
            ###################################################################################################

            $params    = array();
            $remove_column_from_aggregate = array();

            foreach ($request as $var => $value) {
                if (preg_match('/^p-([a-zA-Z0-9_]+)(-[a-z]+)?$/', $var, $matches)) {
                    if ($is_aggregate_query && $value === '*' && ($matches[2] === '' || $matches[2] === '-eq')) {
                        $remove_column_from_aggregate[$matches[1]] = 1;
                    }
                    else {
                        $params["$matches[1]$matches[2]"] = $value;
                    }
                    #echo "param: $matches[1]$matches[2] = $value\n";
                }
            }

            if (isset($request['filter']) && $request['filter']) {
                if (is_array($request['filter'])) {
                    $filters = $request['filter'];
                }
                else {
                    $json = str_replace('\\"','"',$request['filter']);
                    $filters = json_decode($json, true);
                }
                #echo "FILTERS: $filters\n";
                foreach ($filters as $filter) {
                    #echo "FILTER: ", $db->assoc_as_string($filter), "\n";

                    $property = null;
                    if     (isset($filter['property'])) { $property = $filter['property']; }
                    elseif (isset($filter['field']))    { $property = $filter['field']; }

                    if (isset($property)) {

                        $operator = null;
                        if     (isset($filter['operator']))   { $operator = $filter['operator']; }
                        elseif (isset($filter['comparison'])) { $operator = $filter['comparison']; }

                        $value = isset($filter['value']) ? $filter['value'] : null;
                        $type  = isset($filter['type'])  ? $filter['type']  : 'string';
                        if (isset($value) && isset($type)) {
                            if (isset($operator)) {
                                if (($operator === 'lt' || $operator === 'gt') && $type === 'numeric') {
                                    $params["$property-$operator"] = $value;
                                    #echo "FILTER: property=[$property] type=[$type] operator=[$operator] value=[$value] : params[$property-contains]=$value\n";
                                }
                                elseif ($operator === 'eq' || $type === 'string') {
                                    if ($is_aggregate_query && $value === '*') { $remove_column_from_aggregate[$property] = 1; }
                                    else                                       { $params[$property] = $value; }
                                    #echo "FILTER: property=[$property] type=[$type] operator=[$operator] value=[$value] : params[$property]=$value\n";
                                }
                            }
                            else {
                                if (preg_match('/^[A-Za-z \\.]+$/', $value)) {
                                    $params["$property-contains"] = $value;
                                    #echo "FILTER: property=[$property] type=[$type] value=[$value] : params[$property-contains]=$value\n";
                                }
                                else {
                                    if ($is_aggregate_query && $value === '*') { $remove_column_from_aggregate[$property] = 1; }
                                    else                                       { $params[$property] = $value; }
                                    #echo "FILTER: property=[$property] type=[$type] value=[$value] : params[$property]=$value\n";
                                }
                            }
                        }
                    }
                }
            }

            ###################################################################################################
            # Columns
            ###################################################################################################

            $get_options = array();

            if ($is_aggregate_query) {
                $get_options['aggregate'] = 1;
                # Using group_columns is the preferred way of managing the size of the aggregate query (instead of using filter="*")
                # Query only the display_columns which are either (a) aggregate keys and in the group_columns, or (b) not aggregate keys
                if (isset($group_columns)) {
                    if (!isset($table_def)) $table_def = &$db->get_table_def($table, null, $get_options);
#print_r($table_def);
                    $group_columns_array = explode(',', $group_columns); # convert from a comma-separated list to an actual PHP array
#print_r($group_columns_array);
#print_r($data_columns);
                    $limited_columns_array = array();
                    $column_defs = $table_def['column'];
                    foreach ($data_columns as $column_name) {
                        if (isset($column_defs[$column_name]) &&
                            isset($column_defs[$column_name]['agg_key_ind']) &&
                            $column_defs[$column_name]['agg_key_ind'] === 'Y') {
                            if (in_array($column_name, $group_columns_array)) {
                                $limited_columns_array[] = $column_name;
                            }
                        }
                        else {
                            $limited_columns_array[] = $column_name;
                        }
                    }
#print_r($limited_columns_array);
                    if (count($limited_columns_array) > 0) {
                        $data_columns = $limited_columns_array;
                    }
                    else {
                        $data_columns = array('count(1) as num');
                    }
#print_r($data_columns);
                }
                else {
                    # The following chunk of code is only when you put a "*" in the filter field (and don't supply the group_columns)
                    if (count($remove_column_from_aggregate) > 0) {
                        $remaining_columns = array();
                        foreach ($data_columns as $column) {
                            if (! isset($remove_column_from_aggregate[$column])) {
                                $remaining_columns[] = $column;
                            }
                        }
                        $data_columns = $remaining_columns;
                    }
                }
            }

            ###################################################################################################
            # Query Options
            ###################################################################################################

            if (isset($request['sort']) && $request['sort']) {
                #echo "sort=[$request[sort]]\n";
                $sort_param = $request['sort'];
                if (is_array($sort_param) || $sort_param[0] === '[') {   # ]
                    if (is_array($sort_param)) {
                        $sorts = $sort_param;
                    }
                    else {
                        # for some reason, json_decode on PHP 5.3.5 on Mac doesn't decode [{\"var\":\"value\"}]
                        # so we need the following line to get rid of the escaped quotes
                        $sort_param = str_replace('\\"', '"', $sort_param);
                        $sorts = json_decode($sort_param, true);
                    }
                    if (is_array($sorts) && count($sorts) > 0) {
                        $order_by_columns = array();
                        foreach ($sorts as $sort) {
                            if (isset($sort['property']) && $sort['property']) {
                                if (isset($sort['direction'])) {
                                    $direction = $sort['direction'];
                                    if (preg_match('/^(ASC|DESC)$/i', $direction)) {
                                        $order_by_columns[] = "$sort[property].$direction";
                                    }
                                    else {
                                        throw new \Exception('ERROR: the "direction" in a "sort" is not recognized (not "ASC" or "DESC") ($request[sort])');
                                    }
                                }
                                else {
                                    $order_by_columns[] = "$sort[property]";
                                }
                            }
                        }
                        $get_options['order_by'] = $order_by_columns;
                    }
                }
                else {
                    $sorts = explode(',', $sort_param);
                    if (is_array($sorts) && count($sorts) > 0) {
                        $order_by_columns = array();
                        foreach ($sorts as $sort) {
                            if (preg_match('/^([A-Za-z0-9_]+)\\.(asc|desc)$/i', $sort, $matches)) {
                                $order_by_columns[] = $matches[1] . '.' . strtolower($matches[2]);
                            }
                            elseif (preg_match('/^[A-Za-z0-9_]+$/', $sort, $matches)) {
                                $order_by_columns[] = $sort;
                            }
                            else {
                                throw new \Exception('ERROR: not a valid sort spec ($sort) (must be a column name, optionally followed by ".asc" or ".desc")');
                            }
                        }
                        $get_options['order_by'] = $order_by_columns;
                    }
                }
            }

            if (isset($request['start']) && $request['start'] > 0) {
                $start = $request['start'];
                if (! preg_match('/^[0-9]+$/', $start)) throw new \Exception('ERROR: the "start" parameter contains illegal characters (must be a non-negative integer)');
                $get_options['offset'] = $start;
            }

            $limit = (isset($request['limit'])) ? $request['limit'] : 0;
            if (! preg_match('/^[0-9]+$/', $limit)) throw new \Exception('ERROR: the "limit" parameter contains illegal characters (must be a non-negative integer)');
            if ($limit < 0) throw new \Exception('ERROR: the "limit" parameter must be a non-negative integer');
            #if ($limit == 0 || $limit > 1000) $limit = 1000;
            if ($limit) $get_options['limit'] = $limit;

            if (isset($options['test_mode']) && $options['test_mode']) {
                if (isset($request['debug_sql'])) {
                    if (! preg_match('/^-?[0-9]+$/', $request['debug_sql'])) throw new \Exception('ERROR: the "debug_sql" parameter contains illegal characters (must be an integer)');
                    global $debug_sql;
                    $debug_sql = $request['debug_sql'];
                }
                if (isset($request['trace'])) {
                    if (! preg_match('/^-?[0-9]+$/', $request['trace'])) throw new \Exception('ERROR: the "trace" parameter contains illegal characters (must be an integer)');
                    global $trace;
                    $trace = $request['trace'];
                }
            }

            $get_options['sql'] = 1;

#echo "get_rows($fulltable, params, data_columns, get_options)\n";
#print_r($params);
#print_r($data_columns);
#print_r($get_options);
            if ($rowtype === 'array') {
                $data = $db->get_rows($fulltable, $params, $data_columns, $get_options);
            }
            else {
                $data = $db->get_hashes($fulltable, $params, $data_columns, $get_options);

#print_r($request);
                if     (isset($request['keys_horiz']))     { $keys_horiz = $request['keys_horiz']; }
                elseif (isset($get_options['keys_horiz'])) { $keys_horiz = $get_options['keys_horiz']; }
                else                                       { $keys_horiz = null; }

                if (isset($keys_horiz)) {
                    if (!isset($table_def)) $table_def = &$db->get_table_def($table, null, $get_options);
                    if (!isset($columns)) {
                        $save_columns = 1;
                        $columns = array();
                    }
#echo "make_crosstab_columns(data, $keys_horiz, data_columns, params, columns, table_def)\n";
#print_r($data_columns);
#print_r($params);
#print_r($columns);
#print_r($columns);
                    $data = $this->make_crosstab_columns($data, $keys_horiz, $data_columns, $params, $columns, $table_def);
#print_r($columns);
                    if ($save_columns) $table_def['columns'] = $columns;
                }
            }
            $response['data'] = $data;
            $response['sql']  = $get_options['sql'];

            if (!$nocount) {
                $count_options = $get_options;
                if (isset($count_options['offset'])) {
                    $count_options['limit'] = $count_options['offset'] + 1000;
                    unset($count_options['offset']);
                }
                else {
                    $count_options['limit'] = 1000;
                }
                if (isset($count_options['order_by'])) unset($count_options['order_by']);

                $count_sql  = $db->make_select_sql($fulltable, $params, array('1 as num'), $count_options);
                $count_rows = $db->get_rows("select count(*) from ($count_sql) x");
                $count = $count_rows[0][0];

                $response['total'] = $count;
            }
        }
        if ($trace) trace_exit();
    }

    /**
     * make_crosstab_columns ($data, $keys_horiz, $data_columns, $params, &$crosstab_columns, &$table_def = null) : $crosstab_data
     * $data             - IN     - array of associative arrays
     * $keys_horiz       - IN     - scalar (may be a comma-separated list of columns) - columns used for cross-tabulating
     * $data_columns     - IN     - array - column list to be used to retrieve data
     * $params           - IN     - associative array - (may be a comma-separated list of columns)
     * $crosstab_columns - IN/OUT - array - the names of the columns returned after cross-tabulation
     * $table_def        - IN/OUT (optional) - tbd
     * @returns $crosstab_data - OUT - array of associative arrays
     * ISSUES:
     *   1. Crosstab Column Creation (and Limitation)
     *      1a. Column enumeration
     *          1a1. Data-driven (No Limitation)
     *          1a2. Pre-specified (Limited by Predefined Column List)
     *          1a3. Parameterized (Limited by Predefined Parameter Value Lists)
     *      1b. Column ordering (how $crosstab_columns is computed)
     *   2. Key Column Modifiers (date/datetime)
     *      year
     *      month
     *      day
     *      hour   (6hour   = 6 hour intervals)
     *      minute (5minute = 5 minute intervals)
     *      second
     *   3. Crosstab Heading Layout
     */
    private function make_crosstab_columns ($data, $keys_horiz, $data_columns, $params, &$crosstab_columns, &$table_def) {
        global $context, $trace;
        if ($trace) trace_entry();
    #echo "make_crosstab_columns (data, $keys_horiz, data_columns, params, crosstab_columns, table_def)\n";
#print_r($data_columns);
#print_r($params);
#print_r($crosstab_columns);
#print_r($table_def);

        $column_defs =& $table_def['column'];
        $crosstab_data           = array();
        $crosstab_data_idx       = array();
        $crosstab_extra_data     = array();

        if (preg_match('!^([^/]+)/([^/]+)$!', $keys_horiz, $matches)) {
            $keys_horiz          = $matches[1];
            $keys_horiz_columns  = explode(',',$matches[1]);
            $columns_horiz       = explode(',',$matches[2]);
        }
        else {
            $keys_horiz_columns  = explode(',',$keys_horiz);
            $columns_horiz       = array();
        }
        $num_keys_horiz_columns  = count($keys_horiz_columns);
        $num_columns_horiz       = count($columns_horiz);
        $keys_horiz_values       = array();
#print_r($keys_horiz_columns);
#print_r($columns_horiz);

        ###########################################################################################
        # get the list of key columns and value columns
        # the value columns will be exploded out into different columns depending on the values in the crosstab columns
        # Note: all of the $data_columns are accounted for in $keys_vert_columns + $keys_horiz_columns + $value_columns.
        ###########################################################################################
        $keys_vert_columns = array();
        $value_columns = array();
        foreach ($data_columns as $column) {
            if (isset($column_defs[$column]['agg_key_ind']) && $column_defs[$column]['agg_key_ind'] === 'Y') {
                if (!in_array($column, $keys_horiz_columns)) {
                    $keys_vert_columns[] = $column;
                }
            }
            else {
                $value_columns[] = $column;
            }
        }

        ###########################################################################################
        # determine the list of $crosstab_columns based on enumerated values in $params
        # there are three ways that crosstab columns may be known:
        #    1. the list of crosstab columns is supplied as 'columns'
        #    2. the list of crosstab columns is not supplied, but all keys_horiz params have enumerated values
        #    3. the list of crosstab columns is not supplied, and we get the values from the data
        ###########################################################################################
        $crosstab_columns_known = (count($crosstab_columns) == 0) ? false : true;
#echo "crosstab_columns_known=[$crosstab_columns_known]\n";
        if (!$crosstab_columns_known) {               // the column list is not known in advance because columns can be created
            $num_keys_horiz_values_missing = 0;      // how many of the keys_horiz do not have values enumerated in the params?
            foreach ($keys_horiz_columns as $column) {     // look at columns in $keys_horiz for ones whose values aren't known
#echo "keys_horiz_columns: column=[$column]\n";
                if (isset($params["$column-eq"])) {
                    $keys_horiz_values[$column] = array($params["$column-eq"]);                 // value is known by an "eq" op
                }
                elseif (isset($params["$column-in"])) {
                    $keys_horiz_values[$column] = explode(',', $params["$column-in"]);   // values are enumerated by an "in" op
                }
                elseif (isset($params[$column])) {
                    $param_value = $params[$column];
                    if (preg_match('/^=([^=!<>~].*)/', $param_value, $matches)) {
                        $keys_horiz_values[$column] = array($matches[1]);                 // value is known by an "eq" operator
                    }
                    elseif (preg_match('/^[^=!<>~].*,/', $param_value)) {
                        $keys_horiz_values[$column] = explode(',', $param_value);  // values are enumerated in a comma-sep list
                    }
                    else {
                        $num_keys_horiz_values_missing++;                   // we don't know the allowed values for this column
                    }
                }
                else {
                    $num_keys_horiz_values_missing++;                       // we don't know the allowed values for this column
                }
            }
#echo "num_keys_horiz_values_missing=[$num_keys_horiz_values_missing]\n";
            foreach ($keys_vert_columns as $column) {
                $crosstab_columns[] = $column;
            }
            if ($num_keys_horiz_values_missing == 0) {
                $flattened_keys_horiz_values = $this->flatten_multidim_values($keys_horiz_columns, $keys_horiz_values);
#print_r($flattened_keys_horiz_values);
#print_r($crosstab_columns);
#print_r($crosstab_columns);
                foreach ($value_columns as $column) {
#echo "column=[$column] num_columns_horiz=[$num_columns_horiz]\n";
                    if ($num_columns_horiz == 0 || in_array($column, $columns_horiz)) {
                        foreach ($flattened_keys_horiz_values as $value) {
                            $crosstab_column    = $column . "_" . $value;
                            $crosstab_columns[] = $crosstab_column;
                            $column_defs[$crosstab_column] = array('column_name'=>$crosstab_column, 'parent'=>$column);
                        }
#print_r($crosstab_columns);
                        if (count($flattened_keys_horiz_values) > 1) {
                            $crosstab_column    = $column . '_COLSUM';
                            $crosstab_columns[] = $crosstab_column;
                            $column_defs[$crosstab_column] = array('column_name'=>$crosstab_column, 'parent'=>$column);
                            $crosstab_column    = $column . '_COLAVG';
                            $crosstab_columns[] = $crosstab_column;
                            $column_defs[$crosstab_column] = array('column_name'=>$crosstab_column, 'parent'=>$column);
#print_r($crosstab_columns);
                        }
                    }
                    else {
                        #$crosstab_columns[] = $column;
                    }
                }
            }
        }

#print_r($crosstab_columns);
#print_r($keys_vert_columns);
#print_r($keys_horiz_columns);
#print_r($value_columns);
        ###########################################################################################
        # for each row of raw data...
        # (we are cross-tabulating raw data rows into displayable cross-tab rows)
        ###########################################################################################
        foreach ($data as $row) {
#print_r($row);
            ###########################################################################################
            # create the KEY
            ###########################################################################################
            $keys_vert_values = array();
            foreach ($keys_vert_columns as $column) {
                $keys_vert_values[] = isset($row[$column]) ? $row[$column] : null;
            }
            $keys_vert_value = implode('|',$keys_vert_values);
#print_r($keys_vert_values);
#echo "keys_vert_value=[$keys_vert_value]\n";

            ###########################################################################################
            # if this is the first time, create a new crosstab row
            ###########################################################################################
            if (isset($crosstab_data_idx[$keys_vert_value])) {
                $idx = $crosstab_data_idx[$keys_vert_value];
                $crosstab_row       =& $crosstab_data[$idx];
                $crosstab_extra_row =& $crosstab_extra_data[$idx];
            }
            else {
                $idx = count($crosstab_data);
                $crosstab_data[$idx]       = array();
                $crosstab_extra_data[$idx] = array();
                $crosstab_row              =& $crosstab_data[$idx];
                $crosstab_extra_row        =& $crosstab_extra_data[$idx];
                $crosstab_data_idx[$keys_vert_value] = $idx;
                foreach ($keys_vert_columns as $column) {
                    $crosstab_row[$column] = isset($row[$column]) ? $row[$column] : null;
                }
            }

            ###########################################################################################
            # create the crosstab value for this data row
            ###########################################################################################
#echo "keys_horiz=[$keys_horiz] num_keys_horiz_columns=[$num_keys_horiz_columns]\n";
            if ($num_keys_horiz_columns == 1) {
                $crosstab_value = isset($row[$keys_horiz]) ? $row[$keys_horiz] : 'null';
                $crosstab_value = preg_replace('/[^A-Za-z0-9]+/', '_', $crosstab_value);
            }
            else {
                $crosstab_value_array = array();
                foreach ($keys_horiz_columns as $column) {
                    $crosstab_value = isset($row[$column]) ? $row[$column] : 'null';
                    $crosstab_value = preg_replace('/[^A-Za-z0-9]+/g', '_', $crosstab_value);
                    $crosstab_value_array[] = $crosstab_value;
                }
                $crosstab_value = implode('_',$crosstab_value_array);
            }
#print_r($row);
#echo "crosstab_value=[$crosstab_value]\n";

            ###########################################################################################
            # create crosstab columns (based on the value columns and the crosstab value)
            ###########################################################################################
            foreach ($value_columns as $column) {
                $crosstab_column = $column . '_' . $crosstab_value;
                $sum_column      = $column . '_COLSUM';
                $count_column    = $column . '_COLCOUNT';
                $value           = isset($row[$column]) ? $row[$column] : null;
                if (isset($crosstab_row[$crosstab_column])) {
                    throw new \Exception("Error: multiple records with the same keys:  [$keys_vert_value] [$column] [$crosstab_value]");
                }
                else {
#echo "column=[$column] crosstab_column=[$crosstab_column] crosstab_columns_known=[$crosstab_columns_known]\n";
                    if ($crosstab_columns_known) {
                        if (in_array($crosstab_column, $crosstab_columns)) {
                            $crosstab_row[$crosstab_column] = $value;
                        }
                    }
                    else {
                        if ($num_columns_horiz == 0 || in_array($column, $columns_horiz)) {
                            $crosstab_row[$crosstab_column] = $value;
                            if (!in_array($crosstab_column, $crosstab_columns)) {
                                $crosstab_columns[] = $crosstab_column;
                                $column_defs[$crosstab_column] = array('column_name'=>$crosstab_column, 'parent'=>$column);
                            }
                        }
                    }
                }
                if (isset($value)) {
                    if (isset($crosstab_extra_row[$count_column])) {
                        $crosstab_extra_row[$sum_column]  += $value;
                        $crosstab_extra_row[$count_column]++;
                    }
                    else {
                        $crosstab_extra_row[$sum_column]   = $value;
                        $crosstab_extra_row[$count_column] = 1;
                    }
                }
            }
#print_r($crosstab_row);
        }

        ###########################################################################################
        # for each row of crosstab data, add COLSUM columns and COLAVG columns as appropriate
        ###########################################################################################
        for ($idx = 0; $idx < count($crosstab_data); $idx++) {
            $crosstab_row       =& $crosstab_data[$idx];
            $crosstab_extra_row =& $crosstab_extra_data[$idx];
#print_r($value_columns);
            foreach ($value_columns as $column) {
                $sum_column      = $column . '_COLSUM';
                $avg_column      = $column . '_COLAVG';
                $count_column    = $column . '_COLCOUNT';
#echo "count=[$column]\n";
                if (isset($crosstab_extra_row[$count_column])) {
#echo "crosstab_extra_row[count_column]=[$crosstab_extra_row[$count_column]]\n";
#echo "column=[$column]: crosstab_columns_known=[$crosstab_columns_known] num_columns_horiz=[$num_columns_horiz]<br>\n";
                    if ($crosstab_columns_known) {
                        if (in_array($sum_column, $crosstab_columns)) { $crosstab_row[$sum_column] = $crosstab_extra_row[$sum_column]; }
                        if (in_array($avg_column, $crosstab_columns)) { $crosstab_row[$avg_column] = $crosstab_extra_row[$sum_column] / $crosstab_extra_row[$count_column]; }
                    }
                    else {
                        if ($num_columns_horiz == 0 || in_array($column, $columns_horiz)) {
                            if (!in_array($sum_column, $crosstab_columns)) {
                                $crosstab_columns[] = $sum_column;
                                if (!isset($columns_defs[$sum_column])) $column_defs[$sum_column] = array('column_name'=>$sum_column, 'parent'=>$column);
                            }
                            if (!in_array($avg_column, $crosstab_columns)) {
                                $crosstab_columns[] = $avg_column;
                                if (!isset($columns_defs[$avg_column])) $column_defs[$avg_column] = array('column_name'=>$avg_column, 'parent'=>$column);
                            }
                            $crosstab_row[$sum_column] = $crosstab_extra_row[$sum_column];
                            $crosstab_row[$avg_column] = $crosstab_extra_row[$sum_column] / $crosstab_extra_row[$count_column];
                        }
                        else {
                            if (!in_array($column, $crosstab_columns)) {
                                $crosstab_columns[] = $column;
                            }
                            $crosstab_row[$column] = $crosstab_extra_row[$sum_column];
                        }
                    }
                }
            }
        }

        if ($trace) trace_exit($crosstab_data);
        return($crosstab_data);
    }

    private function flatten_multidim_values ($dim_keys, $dim_values, $separator = '_') {
#echo "flatten_multidim_values(dim_keys, dim_values, $separator)\n";
#print_r($dim_keys);
#print_r($dim_values);
        $flattened_values = array();
        $dim_key = array_shift($dim_keys);
        $flattened_values = $dim_values[$dim_key];
        while (count($dim_keys) > 0) {
            $dim_key = array_shift($dim_keys);
            $further_flattened_values = array();
            foreach ($flattened_values as $flattened_value) {
                foreach ($dim_values[$dim_key] as $value) {
                    $further_flattened_values[] = $flattened_value . $separator . $value;
                }
            }
            $flattened_values = $further_flattened_values;
        }
        return($flattened_values);
    }

    public function execute_object_db_table_meta ($http_method, $request, &$response, $db, $table) {
        global $context, $trace;
        if ($trace) trace_entry();
        #debug_print('EXECUTE_OBJECT_DB_TABLE_META:' . print_r($request,true));

        if (isset($request['op'])) $http_method = $this->get_effective_http_method($http_method, $request['op']);

        $get_options = array();
        # [x]view, relationship, [x]aggregate, columns, group_columns, filter, sort, [x]limit)
        if (isset($request['view']))          { $get_options['view']          = $request['view'];          }
        if (isset($request['table_def']))     { $get_options['table_def']     = $request['table_def'];     }
        if (isset($request['relationships'])) { $get_options['relationships'] = $request['relationships']; }

        if (isset($request['columns']))       {
            $columns                      = explode(',',$request['columns']);
            $get_options['columns']       = $columns;
        }

        $table_def   = &$db->get_table_def($table, null, $get_options);

        #if (!$columns && isset($get_options['columns'])) {
        #    $columns = $get_options['columns'];
        #}

        if     (isset($request['keys_horiz']))     { $keys_horiz = $request['keys_horiz']; }
        elseif (isset($get_options['keys_horiz'])) { $keys_horiz = $get_options['keys_horiz']; }
        else                                       { $keys_horiz = null; }

#print_r($get_options);
#echo "keys_horiz=[$keys_horiz]\n";
        if (isset($keys_horiz)) {

            unset($request['view']);
            $request['table']      = $table;
            $request['keys_horiz'] = $keys_horiz;
            $request['meta']       = 1;
            if (!isset($request['group_columns']) && isset($get_options['group_columns'])) { $request['group_columns'] = implode(',',$get_options['group_columns']); }
            if (!isset($request['aggregate'])     && isset($get_options['aggregate']))     { $request['aggregate']     = $get_options['aggregate']; }
            if (!isset($request['filter'])        && isset($table_def['filter']))          { $request['filter']        = $table_def['filter']; }
            if (!isset($request['sort'])          && isset($table_def['sort']))            { $request['sort']          = $table_def['sort']; }
            if (!isset($request['limit'])         && isset($table_def['limit']))           { $request['limit']         = $table_def['limit']; }

            if (!isset($request['columns'])       && isset($get_options['columns']))       { $request['columns']       = $get_options['columns']; }
            if (!isset($request['data_columns'])  && isset($get_options['data_columns']))  { $request['data_columns']  = $get_options['data_columns']; }

            #if (isset($request['data_columns'])) {
            #    if (isset($request['columns'])) {
            #        $data_columns = explode(',',$request['data_columns']);
            #        $columns      = explode(',',$request['columns']);
            #    }
            #    else {
            #        $data_columns = explode(',',$request['data_columns']);
            #        $columns      = array();
            #    }
            #}
            #else {
            #    if (isset($request['columns'])) {
            #        $data_columns = explode(',',$request['columns']);
            #        $columns      = array();
            #    }
            #    else {
            #        throw new Exception('Must specify "columns" and/or "data_columns"', 405);
            #    }
            #}

#print_r($request);
            $query_response = array();
            $this->query($request, $query_response, $db, $table, $table_def);

#echo "Got data\n";
#print_r($query_response['meta']);
#print_r($query_response['data']);
#echo "SQL: ", $query_response['sql'], "\n";

#echo "make_crosstab_columns(data, $keys_horiz, data_columns, params, columns, table_def)\n";
#print_r($data_columns);
#print_r($params);
#print_r($columns);
            #$response['columns'] = $columns;
        }

        if ($http_method == 'get') {
            $response['data'] = $table_def;
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        $response['success'] = true;
        if ($trace) trace_exit();
    }

    public function execute_object_db_table_dbmeta ($http_method, $request, &$response, $db, $table) {
        global $context, $trace;
        if ($trace) trace_entry();

        if (isset($request['op'])) $http_method = $this->get_effective_http_method($http_method, $request['op']);
        if ($http_method === 'get') {
            $response['data'] = $db->get_table_def_from_db($table, $db->get_schema_name());
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        $response['success'] = true;
        if ($trace) trace_exit();
    }

    private function get_effective_http_method ($http_method, $op) {
        global $context, $trace, $options;
        if ($trace) trace_entry();
        if ($http_method === $op) {
            $eff_http_method = $http_method;
        }
        elseif ($http_method === 'post') {
            $eff_http_method = $op;
        }
        elseif ($op === 'get') {
            $eff_http_method = $op;
        }
        else {
            $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? 1 : 0;
            if ($test_mode) {
                $eff_http_method = $op;
            }
            else {
                if ($http_method === 'get') {
                    if     ($op === 'get')    { $eff_http_method = $op; }
                    elseif ($op === 'put')    { throw new \Exception('Method Not Allowed', 405); }
                    elseif ($op === 'delete') { throw new \Exception('Method Not Allowed', 405); }
                    elseif ($op === 'post')   { throw new \Exception('Method Not Allowed', 405); }
                    else                      { throw new \Exception('Method Not Allowed', 405); }
                }
                elseif ($http_method === 'put') {
                    if     ($op === 'get')    { $eff_http_method = $op; }
                    elseif ($op === 'put')    { $eff_http_method = $op; }
                    elseif ($op === 'delete') { throw new \Exception('Method Not Allowed', 405); }
                    elseif ($op === 'post')   { throw new \Exception('Method Not Allowed', 405); }
                    else                      { throw new \Exception('Method Not Allowed', 405); }
                }
                elseif ($http_method === 'delete') {
                    if     ($op === 'get')    { $eff_http_method = $op; }
                    elseif ($op === 'put')    { throw new \Exception('Method Not Allowed', 405); }
                    elseif ($op === 'delete') { $eff_http_method = $op; }
                    elseif ($op === 'post')   { throw new \Exception('Method Not Allowed', 405); }
                    else                      { throw new \Exception('Method Not Allowed', 405); }
                }
                elseif ($http_method === 'post') {
                    if     ($op === 'get')    { $eff_http_method = $op; }
                    elseif ($op === 'put')    { $eff_http_method = $op; }
                    elseif ($op === 'delete') { $eff_http_method = $op; }
                    elseif ($op === 'post')   { $eff_http_method = $op; }
                    else                      { $eff_http_method = $op; }
                }
                else {
                    $eff_http_method = $http_method;
                }
            }
        }
        if ($trace) trace_exit($eff_http_method);
        return($eff_http_method);
    }
}

